use crate::scalar_fn::EvalScalar;

const MAX_ITERS: u64 = 10_000_000_000;
const STEP_SIZE: f64 = 0.5;

#[derive(Clone, Debug)]
pub struct OpOnScalarFns<'a, T>
where
    T: EvalScalar,
{
    func1: &'a T,
    func2: &'a T,
    op: LimitOps,
}

impl<'a, T: EvalScalar> OpOnScalarFns<'a, T> {
    pub fn new(func1: &'a T, func2: &'a T, op: LimitOps) -> Self
    where
        T: EvalScalar,
    {
        OpOnScalarFns { func1, func2, op }
    }
}

impl<'a, T: EvalScalar> EvalScalar for OpOnScalarFns<'a, T> {
    fn eval(&self, x: f64) -> f64 {
        match self.op {
            LimitOps::Add => self.func1.eval(x) + self.func2.eval(x),
            LimitOps::Sub => self.func1.eval(x) - self.func2.eval(x),
            LimitOps::Mul => self.func1.eval(x) * self.func2.eval(x),
            LimitOps::Div => self.func1.eval(x) / self.func2.eval(x),
        }
    }
    fn limit(&self, limit_point: f64) -> Result<f64, LimitError> {
        total_limit(limit_point, self)
    }
    fn get_fn(&self) -> Option<fn(f64, &Option<Vec<f64>>) -> f64> {
        None
    }
    fn get_params(&self) -> Option<Vec<f64>> {
        None
    }
}

#[derive(Clone, Copy, Debug)]
pub enum LimitOps {
    Add,
    Sub,
    Mul,
    Div,
}

#[derive(Clone, Copy, Debug)]
pub enum LimitDirection {
    ApproachFromLeft,
    ApproachFromRight,
}

#[derive(Clone, Copy, Debug)]
pub enum LimitError {
    ConvError,
    DifferentLimits,
    Inf,
    NaN,
    NegInf,
}

pub fn compare_fn_limit_speed<'a, T>(x: f64, func1: &'a T, func2: &'a T) -> Result<f64, LimitError>
where
    T: EvalScalar,
{
    let f1_lim = total_limit(x, func1)?;
    let f2_lim = total_limit(x, func2)?;
    let f1_diff = f1_lim - func1.eval(x);
    let f2_diff = f2_lim - func2.eval(x);
    match f2_diff.abs() > f64::EPSILON {
        true => Ok(f1_diff / f2_diff),
        false => Err(LimitError::NaN),
    }
}

pub fn validate_limit_prop<T>(
    limit_point: f64,
    func1: &T,
    func2: &T,
    prop: LimitOps,
) -> Result<bool, LimitError>
where
    T: EvalScalar,
{
    let func1_limit = func1.limit(limit_point)?;
    let func2_limit = func2.limit(limit_point)?;
    let op_on_funcs = OpOnScalarFns::new(func1, func2, prop);
    let op_on_lims = match prop {
        LimitOps::Add => func1_limit + func2_limit,
        LimitOps::Sub => func1_limit - func2_limit,
        LimitOps::Mul => func1_limit * func2_limit,
        LimitOps::Div => func1_limit / func2_limit,
    };
    let op_on_funcs_limit = op_on_funcs.limit(limit_point)?;
    let tol: f64 = 1.0e-6;
    let same = (op_on_funcs_limit - op_on_lims).abs() < tol;
    if !same {
        println!(
            "Property {:?} disagrees!\n\tOutside: {}, Inside: {}\n",
            prop, op_on_lims, op_on_funcs_limit
        );
    }
    Ok(same)
}

pub fn continuous<T>(limit_point: f64, func: &T) -> bool
where
    T: EvalScalar,
{
    let limit = total_limit(limit_point, func);
    match limit {
        Ok(x) => (x - func.eval(limit_point)).abs() <= 1.0e-8,
        _ => false,
    }
}

pub fn total_limit<T>(limit_point: f64, func: &T) -> Result<f64, LimitError>
where
    T: EvalScalar,
{
    let left = partial_limit(limit_point, func, LimitDirection::ApproachFromLeft)?;
    let right = partial_limit(limit_point, func, LimitDirection::ApproachFromRight)?;
    let abs_diff = (right - left).abs();
    let tol = 1.0e-5_f64;
    match abs_diff <= tol {
        true => Ok(left),
        false => Err(LimitError::DifferentLimits),
    }
}

pub fn partial_limit<T>(
    limit_point: f64,
    func: &T,
    direction: LimitDirection,
) -> Result<f64, LimitError>
where
    T: EvalScalar,
{
    let prefactor = match direction {
        LimitDirection::ApproachFromLeft => -1.0f64,
        LimitDirection::ApproachFromRight => 1.0f64,
    };
    let mut step = STEP_SIZE;

    let mut x = limit_point + prefactor;
    let mut current_value = func.eval(x);
    x = limit_point + prefactor * step;
    let mut next_value = func.eval(x);
    let tol = 1.0e-14_f64;
    let mut iters = 1u64;
    loop {
        if (next_value - current_value).abs() <= tol || iters > MAX_ITERS {
            break;
        }
        current_value = next_value;
        step *= STEP_SIZE;
        x = limit_point + prefactor * step;
        next_value = func.eval(x);
        iters += 1;
    }
    if iters >= MAX_ITERS {
        return Err(LimitError::ConvError);
    };
    if next_value.is_finite() {
        Ok(next_value)
    } else {
        match next_value.is_sign_positive() {
            true => Err(LimitError::Inf),
            false => Err(LimitError::NegInf),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::math::{disco_line, line, log, rational_poly, step};
    use crate::scalar_fn::ScalarFn;

    #[test]
    fn test_left_limit() {
        let limit_point = -1.0f64;
        let params = vec![1.0f64, 1.0f64];
        let line = ScalarFn::new(line, Some(params));
        let result = partial_limit(limit_point, &line, LimitDirection::ApproachFromLeft);
        match result {
            Ok(x) => assert!(x.abs() <= 1.0e-5),
            Err(_) => assert!(false),
        }
    }

    #[test]
    fn test_right_limit() {
        let limit_point = -1.0f64;
        let params = vec![1.0f64, 1.0f64];
        let line = ScalarFn::new(line, Some(params));
        let result = partial_limit(limit_point, &line, LimitDirection::ApproachFromLeft);
        match result {
            Ok(x) => assert!(x.abs() <= 1.0e-5),
            Err(_) => assert!(false),
        }
    }

    #[test]
    fn test_limit_line() {
        let params = vec![1.0f64, 1.0f64];
        let line = ScalarFn::new(line, Some(params));
        let result = total_limit(-1.0f64, &line);
        match result {
            Ok(_) => assert!(true),
            Err(_) => assert!(false),
        }
    }

    #[test]
    fn test_limit_rational_poly() {
        let rational_poly = ScalarFn::new(rational_poly, None);
        let result = total_limit(-1.0f64, &rational_poly);
        match result {
            Ok(x) => {
                println!("rational_poly converged to: {x}");
                assert!(true);
            }
            Err(_) => assert!(false),
        }
    }

    #[test]
    fn test_step_has_no_limit_at_one() {
        let step = ScalarFn::new(step, None);
        let result = total_limit(1.0f64, &step);
        match result {
            Err(LimitError::DifferentLimits) => assert!(true),
            _ => assert!(false),
        }
    }

    #[test]
    fn test_limit_disco_line() {
        let disco_line = ScalarFn::new(disco_line, None);
        let result = total_limit(1.0f64, &disco_line);
        match result {
            Ok(x) => {
                println!("Disco line converged to: {x}");
                assert!(true);
            }
            Err(x) => {
                println!("Disco line failed with: {:?}", x);
                assert!(false);
            }
        }
    }

    #[test]
    fn test_disco_line_is_discontinuous() {
        let disco_line = ScalarFn::new(disco_line, None);
        let result = continuous(1.0f64, &disco_line);
        assert!(!result);
    }

    #[test]
    fn test_step_is_discontinuous() {
        let step = ScalarFn::new(step, None);
        let result = continuous(1.0f64, &step);
        assert!(!result);
    }

    #[test]
    fn test_line_is_continuous() {
        let line = ScalarFn::new(line, None);
        let result = continuous(1.0f64, &line);
        assert!(result);
    }

    #[test]
    fn test_add_limit_prop_line_log_is_valid() {
        let limit_point = 5.0f64;
        let line = ScalarFn::new(line, None);
        let log = ScalarFn::new(log, None);
        let result = validate_limit_prop(limit_point, &line, &log, LimitOps::Add);
        match result {
            Ok(x) => assert!(x),
            Err(x) => {
                println!("Adding limits validation failed with: {:?}", x);
                assert!(false);
            }
        }
    }

    #[test]
    fn test_sub_limit_prop_line_log_is_valid() {
        let limit_point = 5.0f64;
        let line = ScalarFn::new(line, None);
        let log = ScalarFn::new(log, None);
        let result = validate_limit_prop(limit_point, &line, &log, LimitOps::Sub);
        match result {
            Ok(x) => assert!(x),
            Err(x) => {
                println!("Subtracting limits validation failed with: {:?}", x);
                assert!(false);
            }
        }
    }

    #[test]
    fn test_multiply_limit_prop_line_log_is_valid() {
        let limit_point = 5.0f64;
        let line = ScalarFn::new(line, None);
        let log = ScalarFn::new(log, None);
        let result = validate_limit_prop(limit_point, &line, &log, LimitOps::Mul);
        match result {
            Ok(x) => assert!(x),
            Err(x) => {
                println!("Multiplying limits validation failed with: {:?}", x);
                assert!(false);
            }
        }
    }

    #[test]
    fn test_divide_limit_prop_line_log_is_valid() {
        let limit_point = 5.0f64;
        let line = ScalarFn::new(line, None);
        let log = ScalarFn::new(log, None);
        let result = validate_limit_prop(limit_point, &line, &log, LimitOps::Div);
        match result {
            Ok(x) => assert!(x),
            Err(x) => {
                println!("Dividing limits validation failed with: {:?}", x);
                assert!(false);
            }
        }
    }

    #[test]
    fn test_log_slower_than_line() {
        let line = ScalarFn::new(line, None);
        let log = ScalarFn::new(log, None);
        let result = compare_fn_limit_speed(5.0f64, &log, &line);
        match result {
            Ok(x) => assert!(0.0f64 < x && x < 1.0f64),
            Err(x) => {
                println!("Comparing limits failed with: {:?}", x);
                assert!(false);
            }
        }
    }

    #[test]
    fn test_line_not_slower_than_log() {
        let line = ScalarFn::new(line, None);
        let log = ScalarFn::new(log, None);
        let result = compare_fn_limit_speed(5.0f64, &line, &log);
        match result {
            Ok(x) => assert!(x > 1.0f64),
            Err(x) => {
                println!("Comparing limits failed with: {:?}", x);
                assert!(false);
            }
        }
    }

    #[test]
    fn test_line_speed_eq_line() {
        let line = ScalarFn::new(line, None);
        let result = compare_fn_limit_speed(5.0f64, &line, &line);
        match result {
            Ok(x) => assert!((x - 1.0f64).abs() <= 1.0e-6),
            Err(x) => {
                println!("Comparing limits failed with: {:?}", x);
                assert!(false);
            }
        }
    }
}
